/*******************************************************************************************
 * Copyright (c) 2006-2009 Alex Robson, Rob Simmons, Craig Israel, and Rachel Twyford
 *
 * Use of this software in any form requires that you have both 
 * read and agreed to the following terms:
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the "Software"), 
 * to deal in the Software without restriction, including without limitation 
 * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions: 
 *
 * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, 
 * this list of conditions and the following disclaimer in the 
 * documentation and/or other materials provided with the distribution.
 * Neither the name of Nvigorate nor the names of its contributors may 
 * be used to endorse or promote products derived from this software without 
 * specific prior written permission. 
 *
 * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
 * DISCLAIMED. IN NO EVENT SHALL ANY CONTRIBUTOR BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * ****************************************************************************************/

using System;
using System.Collections.Generic;
using System.Linq;
using Ninject.Core.Infrastructure;
using Nvigorate.Extensions;

namespace Nvigorate.Common.Reflection
{
    public class DynamicAccessorCache : IAccessor
    {
        static Dictionary<Tuple<Type, string>, Setter> writerCache = new Dictionary<Tuple<Type, string>, Setter>();
        static Dictionary<Tuple<Type, string>, Getter> readerCache = new Dictionary<Tuple<Type, string>, Getter>();
        static Dictionary<Tuple<Type, string>, Type> expectedTypeLookup = new Dictionary<Tuple<Type, string>, Type>();

        internal const string BackingFieldFormat = "<{0}>k__BackingField";

        private object _lock = new object();

        public void Write(object instance, string memberPath, object value)
        {
            lock (_lock)
            {
                var type = (instance as Type) != null ? instance as Type : instance.GetType();
                var pathComponents = memberPath.Split('.');
                if (pathComponents.Length > 1)
                {
                    var memberName = Enumerable.First(pathComponents);
                    var remainingPath = pathComponents.Skip(1).BuildString(".");

                    var child = Read(instance, memberName);
                    if (child == null)
                    {
                        child = Activator.CreateInstance(GetMemberType(type, memberName));
                        Write(instance, memberName, child);
                    }

                    Write(child, remainingPath, value);
                    return;
                }

                try
                {
                    var setterInfo = GetSetter(type, memberPath);
                    var castValue = MrCastastic.CastIt(setterInfo.Value1, value);
                    setterInfo.Value2.Invoke((instance as Type) != null ? null : instance, castValue);
                }
                catch (Exception e)
                {
                    throw new ReflectorException(e, "The member {0} could not be found on type {1}", memberPath, type.FullName);
                }
            }
        }

        protected Tuple<Type, Setter> GetSetter(Type type, string path)
        {
            var key = Tuple.Create(type, path);
            Setter setter = null;
            Type expectedType = null;
            if (!writerCache.TryGetValue(key, out setter))
            {
                var result = BuildAccessors(key);
                setter = result.Value2;
                expectedType = result.Value3;
            }
            else
            {
                expectedType = expectedTypeLookup[key];
            }
            return Tuple.Create(expectedType, setter);
        }

        public IEnumerable<Tuple<Type, Setter>> GetSettersForMembers(Type type, IEnumerable<string> members)
        {
            return members.Select(m => GetSetter(type, m));
        }

        public object Read(object instance, string memberPath)
        {
            lock (_lock)
            {
                var type = (instance as Type) != null ? instance as Type : instance.GetType();
                var pathComponents = memberPath.Split('.');
                if (pathComponents.Length > 1)
                {
                    var memberName = Enumerable.First(pathComponents);
                    var remainingPath = pathComponents.Skip(1).BuildString(".");

                    var child = Read(instance, memberName);
                    if (child == null)
                        return null;

                    return Read(child, remainingPath);
                }
                try
                {
                    var key = Tuple.Create(type, memberPath);
                    Getter getter = null;
                    if (!readerCache.TryGetValue(key, out getter))
                    {
                        getter = BuildAccessors(key).Value1;
                    }
                    return getter.Invoke((instance as Type) != null ? null : instance);
                }
                catch (Exception e)
                {
                    throw new ReflectorException(e, "The member {0} could not be found on type {1}", memberPath, type.FullName);
                }
            }
        }

        public Type GetMemberType(Type type, string memberPath)
        {
            lock (_lock)
            {
                var pathComponents = memberPath.Split('.');
                if (pathComponents.Length > 1)
                {
                    var memberName = Enumerable.First(pathComponents);
                    var remainingPath = pathComponents.Skip(1).BuildString(".");

                    var child = GetMemberType(type, memberName);
                    if (child == null)
                        return null;

                    return GetMemberType(child, remainingPath);
                }

                try
                {
                    var key = Tuple.Create(type, memberPath);
                    Type expectedType = null;
                    if (!expectedTypeLookup.TryGetValue(key, out expectedType))
                    {
                        expectedType = BuildAccessors(key).Value3;
                    }
                    return expectedType;
                }
                catch (Exception e)
                {
                    throw new ReflectorException(e, "The member {0} could not be found on type {1}", memberPath, type.FullName);
                }
            }
        }

        public void PreLoad(Type type)
        {
            lock (_lock)
            {
                Enumerable.Where(Reflector.GetFieldInfoList(type), f => !f.IsLiteral)
                    .ForEach(f => BuildAccessors(Tuple.Create(type, f.Name)));

                Enumerable.Where(Reflector.GetPropertyInfoList(type), f => f.CanWrite || f.CanWrite)
                    .ForEach(f => BuildAccessors(Tuple.Create(type, f.Name)));
            }
        }

        private Tuple<Getter, Setter, Type> BuildAccessors(Tuple<Type, string> key)
        {
            if (expectedTypeLookup.ContainsKey(key))
                return null;

            Type expectedType = null;
            Type type = key.Value1;
            string memberName = key.Value2;
            bool isProperty = Reflector.HasProperty(type, memberName);
            bool isField = Reflector.HasField(type, memberName);
            Setter writerDelegate = null;
            Getter readerDelegate = null;

            if (isProperty)
            {
                var propertyInfo = Reflector.GetPropertyInfo(type, memberName, true);
                expectedType = propertyInfo.PropertyType;
                //var writerLambdaType = typeof(Action<,,>).MakeGenericType(type, expectedType, typeof(string));
                //var readerLambdaType = typeof(Func<,,>).MakeGenericType(type, typeof(string), expectedType);

                if (propertyInfo.CanWrite)
                {
                    writerDelegate = DynamicMethodFactory.CreateSetter(propertyInfo);
                    //var param1 = Expression.Parameter(type, "T");
                    //var param2 = Expression.Parameter(expectedType, "value");
                    //var param3 = Expression.Parameter(typeof(string), "memberName");
                    //var methodCallExpression = Expression.Call(param1, propertySetter, param2);
                    //writerDelegate = Expression.Lambda(writerLambdaType, methodCallExpression, param1, param2, param3).Compile();
                }
                else if (Reflector.HasField(type, string.Format(BackingFieldFormat, propertyInfo.Name)))
                {
                    isField = true;
                }
                if (!isField && propertyInfo.CanRead)
                {
                    readerDelegate = DynamicMethodFactory.CreateGetter(propertyInfo);
                    //var param1 = Expression.Parameter(type, "T");
                    //var param2 = Expression.Parameter(expectedType, "value");
                    //var param3 = Expression.Parameter(typeof(string), "memberName");
                    //var methodCallExpression = Expression.Call(param1, propertyGetter);
                    //readerDelegate = Expression.Lambda(readerLambdaType, methodCallExpression, param1, param3).Compile();
                }
            }
            if (isField)
            {
                var fieldInfo = Reflector.GetFieldInfo(type, memberName, true);
                expectedType = fieldInfo.FieldType;
                writerDelegate = DynamicMethodFactory.CreateSetter(fieldInfo);
                readerDelegate = DynamicMethodFactory.CreateGetter(fieldInfo);
                //var writerLambdaType = typeof(Action<,,>).MakeGenericType(type, expectedType, typeof(string));
                //var readerLambdaType = typeof(Func<,,>).MakeGenericType(type, typeof(string), expectedType);

                //if (!fieldInfoLookup.ContainsKey(key))
                //    fieldInfoLookup.Add(key, fieldInfo);

                //var param1 = Expression.Parameter(type, "instance");
                //var param2 = Expression.Parameter(expectedType, "value");
                //var param3 = Expression.Parameter(typeof(string), "assignment");
                //var instance = Expression.Constant(this);

                //var callWriteExpression = Expression.Call(instance, "WriteField", new Type[] { type, expectedType }, param1, param2, param3);
                //var callReadExpression = Expression.Call(instance, "ReadField", new Type[] { type, expectedType }, param1, param3);

                //writerDelegate = Expression.Lambda(writerLambdaType, callWriteExpression, param1, param2, param3).Compile();
                //readerDelegate = Expression.Lambda(readerLambdaType, callReadExpression, param1, param3).Compile();
            }
            expectedTypeLookup.Add(key, expectedType);
            writerCache.Add(key, writerDelegate);
            readerCache.Add(key, readerDelegate);
            return Tuple.Create(readerDelegate, writerDelegate, expectedType);
        }
    }
}